package com.heavenick.core.dao;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.MapKey;
import org.apache.ibatis.binding.BindingException;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.SqlSession;

import com.heavenick.core.annotation.DaoMethod;
import com.heavenick.core.annotation.Namespace;
import com.heavenick.core.exception.DaoException;
import com.heavenick.core.model.PageInfoObject;
import com.heavenick.core.util.MapUtil;
import com.heavenick.core.util.StringUtil;

public class DaoProxyMethod {
	private SqlSession sqlSession;
	private Configuration config;
	private MappedStatement mapstmt;
	private ObjectFactory objFactory;

	private Method method;
	private Class<?> declaringInterface;

	private String namespace = "";
	private String sqlId;

	private SqlCommandType type;

	private boolean returnMap;
	private boolean returnMany;
	private boolean returnVoid;

	private String mapKey;

	private Integer pageInfoIndex;
	private Integer resultHandlerIndex;
	private Integer paramIndex;

	public DaoProxyMethod(Class<?> declaringInterface, Method method,
			SqlSession sqlSession) {
		this.method = method;
		this.declaringInterface = declaringInterface;
		this.sqlSession = sqlSession;
		this.config = this.sqlSession.getConfiguration();
		this.objFactory = config.getObjectFactory();
		setCommandName();
		setMethodSignature();
		validateStmtAndSetupCommandType();
	}

	public Object excutor(Object[] args) {
		Object result = null;
		switch (type) {
		case INSERT:
			result = sqlSession.insert(getCommandName(), args[paramIndex]);
			break;
		case UPDATE:
			result = sqlSession.update(getCommandName(), args[paramIndex]);
			break;
		case DELETE:
			result = sqlSession.delete(getCommandName(), args[paramIndex]);
			break;
		case SELECT:
			result = localSelect(args);
			break;
		default:
			throw new DaoException("Unknown execution method for: "
					+ getCommandName());
		}

		return result;
	}

	private Object localSelect(Object[] args) {
		Object result = null;
		if (returnVoid && resultHandlerIndex != null) {
			executeWithResultHandler(args);
		} else if (returnMany) {
			result = executeForMany(args);
		} else if (returnMap) {
			result = executeForMap(args);
		} else {
			result = sqlSession.selectOne(getCommandName(), args[paramIndex]);
		}
		return result;
	}

	private void executeWithResultHandler(Object[] args) {
		if (pageInfoIndex != null) {
			Map<Object, Object> iMap = MapUtil.populateMap(args[paramIndex]);
			MapUtil.populateMap(iMap,args[pageInfoIndex]);
			sqlSession.select(getCommandName(), iMap,(ResultHandler) args[resultHandlerIndex]);
		}
		else{
			sqlSession.select(getCommandName(), args[paramIndex],(ResultHandler) args[resultHandlerIndex]);
		}
		
	}

	private <E> Object executeForMany(Object[] args) {
		List<E> result;
		if (pageInfoIndex != null) {
			Map<Object, Object> iMap = MapUtil.populateMap(args[paramIndex]);
			MapUtil.populateMap(iMap,args[pageInfoIndex]);
			result = sqlSession.<E> selectList(getCommandName(), iMap);
		} else {
			result = sqlSession.<E> selectList(getCommandName(), args[paramIndex]);
		}
		if (!method.getReturnType().isAssignableFrom(result.getClass())) {
			if (method.getReturnType().isArray()) {
				return convertToArray(result);
			} else {
				return convertToDeclaredCollection(result);
			}
		}
		return result;
	}

	private <E> Object convertToDeclaredCollection(List<E> list) {
		Object collection = objFactory.create(method.getReturnType());
		MetaObject metaObject = config.newMetaObject(collection);
		metaObject.addAll(list);
		return collection;
	}

	@SuppressWarnings("unchecked")
	private <E> E[] convertToArray(List<E> list) {
		E[] array = (E[]) Array.newInstance(method.getReturnType()
				.getComponentType(), list.size());
		array = list.toArray(array);
		return array;
	}

	private <K, V> Map<K, V> executeForMap(Object[] args) {
		if (pageInfoIndex != null) {
			Map<Object, Object> iMap = MapUtil.populateMap(args[paramIndex]);
			MapUtil.populateMap(iMap,args[pageInfoIndex]);
			return sqlSession.<K, V> selectMap(getCommandName(), iMap, mapKey);
		}
		return sqlSession.<K, V> selectMap(getCommandName(), args[paramIndex], mapKey);
	}

	/**
	 * set field info with method ;
	 */
	private void setCommandName() {
		Namespace namespaceAnnotation = declaringInterface.getAnnotation(Namespace.class);
		if (namespaceAnnotation != null) {
			namespace = namespaceAnnotation.value() + ".";
		}
		
		sqlId = method.getName();
		DaoMethod daomethod = method.getAnnotation(DaoMethod.class);
		if (daomethod != null) {
			sqlId = StringUtil.isNotNullOrEmpty(daomethod.sqlId()) ? sqlId : daomethod.sqlId();
			namespace = StringUtil.isNullOrEmpty(daomethod.namespace()) ? namespace : daomethod.namespace() + ".";
		}
	}

	private void setMethodSignature() {
		if (method.getReturnType().equals(Void.TYPE)) {
			returnVoid = true;
		}
		if (objFactory.isCollection(method.getReturnType())
				|| method.getReturnType().isArray()) {
			returnMany = true;
		}
		if (Map.class.isAssignableFrom(method.getReturnType())) {
			final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class);
			if (mapKeyAnnotation != null) {
				mapKey = mapKeyAnnotation.value();
				returnMap = true;
			}
		}

		final Class<?>[] argTypes = method.getParameterTypes();
		for (int i = 0; i < argTypes.length; i++) {
			if (PageInfoObject.class.isAssignableFrom(argTypes[i])) {
				if (pageInfoIndex == null) {
					pageInfoIndex = i;
				} else {
					throw new DaoException(method.getName()
							+ " cannot have multiple PageInfoObject parameters");
				}
			} else if (ResultHandler.class.isAssignableFrom(argTypes[i])) {
				if (resultHandlerIndex == null) {
					resultHandlerIndex = i;
				} else {
					throw new DaoException(method.getName()
							+ " cannot have multiple ResultHandler parameters");
				}
			} else {
				if (paramIndex == null)
					paramIndex = i;
				else
					throw new DaoException(method.getName()
							+ " cannot have multiple parameters");
			}
		}
	}

	private void validateStmtAndSetupCommandType() {
		try {
			mapstmt = config.getMappedStatement(getCommandName());
		} catch (Exception e) {
			throw new DaoException("Invalid bound statement (not found): "
					+ getCommandName(), e);
		}
		type = mapstmt.getSqlCommandType();
		if (type == SqlCommandType.UNKNOWN) {
			throw new DaoException("Unknown execution method for: "
					+ getCommandName());
		}
	}

	/**
	 * get sqlmap id from namespace and sqlId
	 * 
	 * @return
	 */
	private String getCommandName() {
		return namespace + sqlId;
	}
}
